Day 22 我終於把一件以前「會用但沒真的想過」的事情補起來:
setCount(prev => prev + 1)
裡面的:
prev => prev + 1
不是什麼 React 專用語法。
它就是一個 function。
而且 JavaScript 的 function 本身可以是一個值,所以才能:
const addOne = number => number + 1
也能:
setCount(addOne)
或:
numbers.map(addOne)
到這裡好像很合理。
結果我往回看自己的程式,又看到三種長得很像的東西:
function add(a, b) {
return a + b
}
const add = function (a, b) {
return a + b
}
以及:
const add = (a, b) => {
return a + b
}
甚至還可以縮成:
const add = (a, b) => a + b
然後我就卡住了。
如果它們最後都可以做到 add(1, 2),JavaScript 到底為什麼需要這麼多種?
難道真的只是:
舊寫法
↓
比較新的寫法
↓
比較潮的寫法
嗎?
好像不是。
今天先記這句:
Function declaration、function expression、arrow function 都能建立 function,但它們不是單純三種不同外觀;建立方式、可以出現的位置,以及部分行為都不完全一樣。
最粗略可以先這樣看:
function declaration
↓
直接宣告一個具名 function
function expression
↓
在 expression 裡產生一個 function value
arrow function
↓
也是 function expression
但語法更短,而且部分語意和傳統 function 不同
所以:
function add() {}
和:
const add = () => {}
不是單純把 function 換成 =>。
全部都做同一件事情:1 + 2。
function add(a, b) {
return a + b
}
console.log(add(1, 2))
結果是 3。
const add = function (a, b) {
return a + b
}
console.log(add(1, 2))
也是 3。
const add = (a, b) => {
return a + b
}
console.log(add(1, 2))
還是 3。
甚至:
const add = (a, b) => a + b
依然可以得到 3。
如果只做這個實驗,真的會得到一個很合理的錯覺:
三個根本一樣啊。
所以問題不能只問「它們能不能算 1 + 2」,而要問:
JavaScript 到底是用什麼方式建立這三個 function?
先看第一種:
function add(a, b) {
return a + b
}
這叫 Function Declaration。
可以先把它想成:
我要在這個 Scope 裡宣告一個叫 add 的 function。
概念上:
function add(...) {...}
↓
建立 function
↓
名稱 add 指向這個 function
這也是很多教學最先教的 function。
因為它很直白:
function calculateTotal(price, quantity) {
return price * quantity
}
只看名字就知道這裡定義了一個叫 calculateTotal 的功能。
再看:
const add = function (a, b) {
return a + b
}
這裡其實可以拆成兩件事情。
右邊:
function (a, b) {
return a + b
}
是一個 function value。
左邊:
const add =
則是在建立一個變數 binding。
所以概念上比較像:
建立一個 function value
↓
把它放進 add
這和 Day 22 的心智模型就接起來了。
例如:
const name = 'Anthony'
可以看成:
字串值
↓
放進 name
而:
const add = function (a, b) {
return a + b
}
則是:
function value
↓
放進 add
Function expression 這個名字突然就比較有意思。
它是一個 expression,也就是會產生一個值的東西,而它產生的值剛好是 function。
例如:
const add = function (a, b) {
return a + b
}
function 後面沒有名字。
這是一個匿名 function expression。
但也可以寫成:
const add = function calculate(a, b) {
return a + b
}
這時 function 本身有名字 calculate,而外面的變數仍然是 add。
今天先不深入這個名字到底在哪個 Scope 裡有效,不然我們會馬上掉進下一個坑。
先知道:
Function expression 可以產生具名或匿名的 function value。
就夠了。
現在回頭看最常出現在 React 裡的:
const add = (a, b) => {
return a + b
}
它叫 Arrow Function Expression。
注意最後那個 Expression。
它跟:
const add = function (a, b) {}
有一個重要共同點。
右邊都是在產生一個 function value。
所以:
const add = (a, b) => a + b
概念上也是:
建立一個 function value
↓
放進 add
這也解釋了為什麼可以直接:
numbers.map(number => number * 2)
因為:
number => number * 2
本身就會產生一個 function。
我甚至不需要先:
const double = number => number * 2
才能傳給 map。
可以直接:
建立 function
↓
立刻當 argument 傳給 map
這就是 Day 22 說的:
Function 是值。
這是我以前最大的誤解之一。
例如:
function add(a, b) {
return a + b
}
可以改成:
const add = (a, b) => {
return a + b
}
又可以變成:
const add = (a, b) => a + b
所以真的很容易得到結論:
Arrow function 就只是 function 的短寫法。
但其實不是。
Arrow function 除了語法比較短之外,還有一些真正的行為差異。
例如它:
沒有自己的 this
沒有自己的 arguments
不能拿來當 constructor
也就是不能把:
function User(name) {
this.name = name
}
無腦改成:
const User = name => {
this.name = name
}
然後期待:
new User('Anthony')
照樣運作。
不會。
所以:
Arrow function 不是 traditional function 的單純短寫。
它有自己的語意。
這篇先不把 this 拆開,光 this 本身就足夠再寫一篇。
回頭看我們已經寫過的:
numbers.map(number => number * 2)
如果不用 arrow,也可以:
numbers.map(function (number) {
return number * 2
})
兩個都可以。
但 callback 常常只是很小的一段邏輯:
拿到一個 number
↓
乘以 2
↓
回傳
寫:
number => number * 2
非常直接。
所以 arrow function 很常出現在 map、filter、find、setTimeout、Promise、React event、React state updater 這類需要 callback 的地方。
這裡的重点不是 Arrow 比 function 高級,而是:
我現在需要的是一個小型 function value,直接傳給另一個 API。
Arrow function 剛好非常適合這種寫法。
Function declaration 看起來像一個獨立宣告:
function sayHello() {
console.log('Hello')
}
Function expression 則可以出現在需要 value 的地方:
const sayHello = function () {
console.log('Hello')
}
也能:
setTimeout(function () {
console.log('Hello')
}, 1000)
Arrow function 一樣:
setTimeout(() => {
console.log('Hello')
}, 1000)
因為 expression 可以產生值,所以 function expression 和 arrow function 才能直接被 setTimeout 這類 API 收到。
做一個小實驗。
sayHello()
function sayHello() {
console.log('Hello')
}
Function 明明寫在下面,卻可以先呼叫。
再改成:
sayHello()
const sayHello = function () {
console.log('Hello')
}
這次就不行。
Arrow function 也是:
sayHello()
const sayHello = () => {
console.log('Hello')
}
也不行。
這又證明了一次:
三種寫法不只是長得不一樣。
但是今天我先不要直接丟一句「因為 hoisting」然後假裝事情講完了。
因為 Hoisting 本身又是一個超容易被教成「JavaScript 把程式碼搬到最上面」的東西。
今天只先留下觀察:
Function Declaration
可以出現宣告前呼叫的行為
const + Function Expression
不行
const + Arrow Function
也不行
原因之後再說。
我現在不想把它背成 A 情境只能用 declaration、B 情境只能用 expression、C 情境只能用 arrow。
很多時候確實有不同寫法可以達到目的。
比較有用的思考方式是:
function calculateTotal(price, quantity) {
return price * quantity
}
Function declaration 很直觀。
const handler = function () {
console.log('click')
}
Function expression 的心智模型很直接:
function value
↓
交給變數
items.map(item => item.name)
Arrow function 很自然。
除非那個 function 本身值得被命名和重複使用。
以前剛學 React 的時候,很容易出現一個錯覺:
React 的函式是不是都要寫 =>?
其實不是。
是因為 React 裡面非常常做:
把 function 當值傳出去
例如:
<button onClick={() => setCount(count + 1)}>
又或者:
setCount(prev => prev + 1)
再例如:
users.map(user => <UserCard user={user} />)
全部都需要一個 function value。
而 arrow function 寫這種小 callback 很方便。
不是 React 發明了 arrow function,而是 React 大量利用 JavaScript:
Function 可以當成值。
Day 17:
onClick={handleClick}
為什麼不是:
onClick={handleClick()}
因為一個是在傳 function,一個是在現在就呼叫。
Day 21:
setCount(prev => prev + 1)
React 收到的是 updater function。
Day 22:
prev => prev + 1
為什麼 function 可以塞進另一個 function?
因為 function 本身就是 value。
Day 23:
function add() {}
const add = function () {}
const add = () => {}
為什麼又有這麼多種?
因為 JavaScript 提供不同方式建立 function,而它們的使用位置和部分語意並不完全相同。
現在這條線終於比較像一整塊 JavaScript,而不是 React 的一堆特殊規則了。
不是。
Arrow function 比較短,但它也有不同的語意,例如沒有自己的 this,也不能當 constructor。
不一定。
const fn = function namedFunction() {}
Function expression 也能有自己的名字。
如果只是輸入兩個數字、相加、return,看起來確實很像。
但宣告時機、this、arguments、constructor、expression context 都可能出現差異。
沒有。
React 只是很常需要 callback,而 arrow function 很適合建立短小的 callback value。
Function declaration 是直接宣告 function;function expression 和 arrow function 則可以在 expression 裡產生 function value,而 arrow function 又具有和 traditional function 不完全相同的語意。
所以三種寫法不是古代、近代、現代三個版本,而是 JavaScript 提供不同建立 Function 的方式:基本功能有重疊,但使用位置與行為有部分差異。
不過今天寫到這裡,我發現一直偷偷出現一個詞:
Scope
像:
const add = function calculate() {}
calculate 到底在哪裡看得到?
如果我把:
const name = 'Anthony'
放進 function 裡,外面又為什麼看不到?
甚至:
if (true) {
const count = 10
}
為什麼出了大括號就不能用了?
所以接下來想問的是:
Scope 到底在限制什麼?JavaScript 為什麼不能讓每個變數在任何地方都看得到?